package database;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.spec.KeySpec;
import java.util.InvalidPropertiesFormatException;
import java.util.Properties;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import exceptions.database.EncriptingException;
import exceptions.database.InfoAlreadyExistsException;
import exceptions.database.InvalidInfoException;

import sun.misc.BASE64Encoder;

/**
 * Class that manage the password, this info is stored in shadow.xml file. It
 * stores a password, remove and verify if a password is a password of an UID.
 * 
 * It uses a 64 bits cryptography, with a fixed key, it makes it insecure. But,
 * it will be fixed in others versions.
 * 
 * @author htrmeira@gmail.com
 * 
 */
public class ManagePassword {
	private final String PATH = "Profiles/shadow.xml";
	private static final String ALGORITHM = "PBEWithMD5AndDES";
	private static ManagePassword thisInstance;
	private Properties passwords;
	private BASE64Encoder encoder;
	private KeySpec keySpected;
	private SecretKeyFactory convertedKey;
	private SecretKey secretKey;
	private PBEParameterSpec parameterSpecified;
	private String uid;

	/**
	 * A constructor of a ManagePassword. Initialize the uid variable with the
	 * value passed in the argument, load the passwords to memory and prepare to
	 * encrypt passwords variables.
	 * 
	 * @param uid
	 *            The UID of the the user to be manipulated.
	 * @throws EncriptingException
	 *             If something nasty occurs while encrypting the password.
	 * @throws InvalidInfoException
	 *             If the UID passed is null, empty or has just white spaces.
	 * @throws IOException
	 *             If something nasty occurred while manipulates shadow.xml
	 * @throws InvalidPropertiesFormatException
	 *             If shadow.xml is corrupted.
	 */
	public ManagePassword() throws EncriptingException, InvalidPropertiesFormatException, IOException {
		this.uid = null;
		
		prepareCripto();
		loadPasswords();
	}
	
	public void clear() throws EncriptingException,
			InvalidPropertiesFormatException, IOException {
		
		thisInstance = new ManagePassword();
	}
	
	/**
	 * Prepare the variables to encrypt, the algorithm to use is instantiated, a
	 * secret key is created based on a given static key.
	 * 
	 * @throws EncriptingException
	 *             If something nasty occurs while encrypting the password.
	 */
	private void prepareCripto() throws EncriptingException {
		final String key = "RNBDjha987jhsG;,aS;87R2J3G8&ajh{gf(a&W:;sa$;";
		encoder = new BASE64Encoder();
		keySpected = new PBEKeySpec(key.toCharArray());
		try {
			convertedKey = SecretKeyFactory.getInstance(ALGORITHM);
			secretKey = convertedKey.generateSecret(keySpected);
		} catch (Exception e) {
			throw new EncriptingException(
					"Some thing nasty occurred while encripting the password. Please, fix it.");
		}
		
		parameterSpecified = new PBEParameterSpec(new byte[] { 3, 1, 4, 1, 5,
				9, 2, 6 }, 20);
	}
	
	/**
	 * Set the informations to be manipulated to this UID.
	 * 
	 * @param uid
	 *            The UID of the password to be manipulated.
	 * @throws InvalidInfoException
	 *             If the UID is null, empty or has just white spaces.
	 */
	public void setUser(String uid) throws InvalidInfoException {
		if (!isValidString(uid)) {
			throw new InvalidInfoException("Please, choose a valid uid");
		}
		this.uid = uid;
	}

	/**
	 * Create a new password to an UID. And to change a password without
	 * verification.
	 * 
	 * @param password
	 *            The password to be created.
	 * @throws InvalidInfoException
	 *             If the password is null, empty or has just white spaces.
	 * @throws EncriptingException
	 *             If something nasty occurs while encrypting password.
	 * @throws IOException
	 *             If something nasty occurred while manipulates shadow.xml.
	 * @throws InvalidPropertiesFormatException
	 *             If the shadow.xml is corrupted.
	 */
	public void setPassword(String password) throws InvalidInfoException,
			EncriptingException, InvalidPropertiesFormatException, IOException {
		
		if (!isValidString(password)) {
			throw new InvalidInfoException("Please, choose a valid password");
		}
		
		passwords.setProperty(uid, encript(password));
		loadPasswords();
	}
	
	/**
	 * 
	 * @param password
	 * @throws InvalidPropertiesFormatException
	 * @throws InvalidInfoException
	 * @throws InfoAlreadyExistsException
	 * @throws EncriptingException
	 * @throws IOException
	 */
	public void createPassword(String password)
			throws InvalidPropertiesFormatException, InvalidInfoException,
			InfoAlreadyExistsException, EncriptingException, IOException {
		
		setPassword(password);
	}

	/**
	 * Change an existent password verifying the current password. It check if
	 * the given current password if right, and than changes it.
	 * 
	 * @param currentPassword
	 *            The current password to be verified.
	 * @param newPassword
	 *            The new password of this user.
	 * 
	 * @throws InvalidInfoException
	 *             If the new password is null, empty or just white spaces.
	 * @throws EncriptingException
	 *             It something nasty occurred while encrypting passwords.
	 * 
	 * @return true if the change occurred with success, false otherwise.
	 * @throws IOException 
	 * @throws InvalidPropertiesFormatException 
	 */
	public boolean changePassword(String currentPassword, String newPassword)
			throws InvalidInfoException, EncriptingException,
			InvalidPropertiesFormatException, IOException {
		
		if (!isValidString(newPassword)) {
			throw new InvalidInfoException("Please, choose a valid password");
		}
		
		if(isUserPassword(currentPassword)) {
			passwords.setProperty(uid, encript(newPassword));
			return true;
		}
		
		return false;
	}
	
	/**
	 * Verify if a password is the password of this UID. To verify it, encrypting the given password and compare if is equals to the encrypted password.
	 * 
	 * @param password The password to be verified.
	 * 
	 * @throws EncriptingException If something nasty occurred while encrypting the password.
	 * 
	 * @return true if the password match, false otherwise.
	 */
	public boolean isUserPassword(String password) throws EncriptingException {
		String encriptedPassword = passwords.getProperty(uid);
		return encriptedPassword.equals(encript(password));
	}
	
	public static boolean checkPassword(String uid, String password)
			throws EncriptingException, InvalidInfoException,
			InvalidPropertiesFormatException, IOException {
		
		ManagePassword mp = new ManagePassword();
		mp.setUser(uid);
		return mp.isUserPassword(password);
	}
	
	/**
	 * This method provide Singleton pattern to this class. Would be bad to the
	 * program with two different instances of this modifying the same file.
	 * 
	 * @return An instance of this class, if the class had not been
	 *         instantiated, it will do. with the class had already been
	 *         instantiated will return this instance.
	 * @throws InvalidInfoException
	 *             If the given UID is null, empty or just white spaces.
	 * @throws EncriptingException
	 *             If something nasty occurred while initializing the encrypting
	 *             variables.
	 * @throws IOException
	 *             If something nasty occurred while handle the shadow.xml
	 * @throws InvalidPropertiesFormatException
	 *             If shadow.xml is corrupted.
	 */
	public static ManagePassword getInstance()
			throws EncriptingException, InvalidPropertiesFormatException, IOException {
		if (thisInstance == null) {
			thisInstance = new ManagePassword();
		}
		
		return thisInstance;
	}
	
	/**
	 * Stores the all the passwords to the file, it works like a password file
	 * update.
	 * 
	 * @throws InvalidPropertiesFormatException
	 *             If the shadow.xml is corrupted.
	 * @throws IOException
	 *             If something nasty occurred while manipulating the shadow.xml
	 *             file.
	 */
	public void save() throws InvalidPropertiesFormatException, IOException {
		try {
			PrepareFiles.prepareFile(PATH,
					"This file stores the passwords of the users.");
			FileOutputStream outputFile = new FileOutputStream(PATH);
			passwords.storeToXML(outputFile,
					"This file stores the passwords of the users.");
			outputFile.close();
			
		} catch (InvalidPropertiesFormatException e) {
			throw new InvalidPropertiesFormatException("your shadow.xml is corrupted.");
		} 
	}
	
	/**
	 * 
	 * @return
	 */
	public boolean passwordExists() {
		return passwords.containsKey(uid);
	}
	
	/**
	 * This method is responsible for encrypting the password.
	 * 
	 * @param word
	 *            the word to be encrypted.
	 * @return the word encrypted.
	 * @throws EncriptingException
	 *             if something nasty occurred while encrypting the word.
	 */
	private String encript(String word) throws EncriptingException {
		Cipher cipher;
		try {
			cipher = Cipher.getInstance(ALGORITHM);
			cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpecified);
			return encoder.encode(cipher.doFinal(word.getBytes()));
		} catch (Exception e) {
			throw new EncriptingException("Some thing nasty occurred while encripting the password. Please, fix it.");
		}
	}
	
	/**
	 * This method load all the password in shadow.xml to memory.
	 * 
	 * @throws IOException
	 *             If something nasty occurred while manipulating the shadow.xml
	 *             file.
	 * @throws InvalidPropertiesFormatException
	 *             If the shadow.xml is corrupted.
	 */
	private void loadPasswords() throws InvalidPropertiesFormatException, IOException {
		try {
			PrepareFiles.prepareFile(PATH);
			FileInputStream in = new FileInputStream(PATH);
			passwords = new Properties();
			passwords.loadFromXML(in);
			in.close();
			
		} catch (InvalidPropertiesFormatException e) {
			throw new InvalidPropertiesFormatException("your shadow.xml is corrupted.");
		}
	}
	
	/**
	 * Remove a password of the shadow.xml. It verify if the given password
	 * match.
	 * 
	 * @param password
	 *            The password to be removed.
	 * @throws EncriptingException
	 *             If something nasty occurred while encrypting the password.
	 * @return true if the given password match, false otherwise.
	 */
	public boolean removePassword(String password) throws EncriptingException {
		if(isUserPassword(password)) {
			passwords.remove(uid);
			return true;
		}
		return false;
	}
	
	/**
	 * Remove a password without check the password.
	 * @return true with this UID exists in the file, false otherwise.
	 */
	public boolean forceRemovePassword() {
		if(passwords.containsKey(uid)){
			passwords.remove(uid);
			return true;
		}
		return false;
	}
	
	/**
	 * This method verify if a <code>String</code> is valid. A
	 * <code>String</code> is a valid when is not null and is not full just with
	 * white spaces.
	 * 
	 * @param toCheck
	 *            The <code>String</code> to verify.
	 * @return true, if it is a valid String false, if it is not.
	 */
	private boolean isValidString(String toCheck) {
		if (toCheck == null) {
			return false;
		}

		char[] charateres = toCheck.toCharArray();

		for (char character : charateres) {
			if (character != ' ') {
				return true;
			}
		}
		return false;
	}
	
	public static String encriptStaticPassword(String word) throws EncriptingException {
		final String ALGORITHM2 = "PBEWithMD5AndDES";
		BASE64Encoder encoder2;
		KeySpec keySpected2;
		SecretKeyFactory convertedKey2;
		SecretKey secretKey2;
		PBEParameterSpec parameterSpecified2;
		
		final String key = "RNBDjha987jhsG;,aS;87R2J3G8&ajh{gf(a&W:;sa$;";
		encoder2 = new BASE64Encoder();
		keySpected2 = new PBEKeySpec(key.toCharArray());
		try {
			convertedKey2 = SecretKeyFactory.getInstance(ALGORITHM2);
			secretKey2 = convertedKey2.generateSecret(keySpected2);
		} catch (Exception e) {
			throw new EncriptingException(
					"Some thing nasty occurred while encripting the password. Please, fix it.");
		}
		
		parameterSpecified2 = new PBEParameterSpec(new byte[] { 3, 1, 4, 1, 5,
				9, 2, 6 }, 20);
		
		Cipher cipher;
		try {
			cipher = Cipher.getInstance(ALGORITHM);
			cipher.init(Cipher.ENCRYPT_MODE, secretKey2, parameterSpecified2);
			return encoder2.encode(cipher.doFinal(word.getBytes()));
		} catch (Exception e) {
			throw new EncriptingException("Some thing nasty occurred while encripting the password. Please, fix it.");
		}
	}
}